文章目录
  1. 1. SharedPreference实现
  2. 2. SharedPreference优缺点
  3. 3. 结语

Android为各位开发者提供了简单的数据存储和交换的方式, 就是SharedPreference, 它是一个在应用数据目录下的.xml文件, 无论是从名字还是实际应用来看, 它都是存取轻量不涉及安全问题的数据(例如设置项)的趁手方式之一. 当然, 它采用xml这样的格式有其自己的考虑, 但未必是最合适的. 当然它的缺点也跟优点一样明显, 今天我们就来看看它在源码中是怎样实现的, 由此也可看出它的优缺点何在.
老方法, 我们从实际使用场景来倒推.
本文结构:

  1. SharedPreference实现
  2. SharedPreference优缺点

SharedPreference实现

一个简单的读写场景通常用到getSharedPreferences接口, 和SharedPreferences.Editor. 例如:

1
2
3
4
5
6
7
8
9
10
// TextView ttvv has been initiated before
SharedPreferences sp = getSharedPreferences("Test", MODE_PRIVATE);
sp.edit().putBoolean("Another test key", true).commit();
Map<String, ?> content = sp.getAll();
StringBuilder sb = new StringBuilder();
for(String key : content.keySet()){
sb.append(String.format("%s=%s\n", key, content.get(key).toString()));
}
ttvv.setText("SharedPreference测试:\n ");
ttvv.append(sb.toString());

首先追踪getSharedPreferences发现它调用的是ContextWrapper.mBase.getSharedPreferences, mBase这个东西我在另一篇分析Context的博客中讲过, 这里不赘述, 我们直接看它的实现, 发现它返回的是一个SharedPreferencesImpl实例, 其首先在静态成员变量sSharedPrefs中获取以传入的参数name为key的实例, 如果没有则在一个固定目录getPreferencesDir()(默认是shared_prefs)以传入的参数(name)创建一个新的SharedPreference文件name.xml(默认实现中, name不能包含路径分隔符), 然后以(name, mode)创建一个SharedPreferencesImpl实例. 第一个参数很好理解, 就是SharedPreference文件的文件名, 第二个参数指的是文件创建模式, 可选的值在Context抽象类中有定义, 主要是MODE_PRIVATE(默认文件打开方式就是这个, 即仅有创建者所在的应用或共享该应用User ID的其他应用有访问权限) MODE_WORLD_READABLE等常量, 在SharedPreference写的时候会根据创建模式判断是否有写权限.
SharedPreferencesImpl构造方法中, 开启了一个线程从文件系统中载入指定名称的SharedPreference文件(构造方法中会先备份文件, 载入时如果存在备份文件, 先删除当前文件, 再从备份文件恢复), 并解析成Map存在成员变量mMap中. 可见, 我们实际获得的就是一个普通的xml文件解析成的HashMap.
以上, 我们知道了一个SharedPreference文件的创建和读取. 至于edit()接口返回的Editor接口很好理解, 它就是提供了对SharedPreference文件操作的封装, 它的默认实现是SharedPreferencesImpl.EditorImpl, 基本上就是同步地读写mModified成员变量, 它是Maps.newHashMap创建的实例, 它只存储有更改的键值对. 这里我们需要重点关注Editor两个地方的实现boolean commit()void apply().
相信用过SharedPreference的同学都知道, 这两个接口都是用来提交变化到文件的, 但具体提交过程可能不是人人都清楚. 我们先来看两个接口的说明, 通常源码里面会有重要信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* Commit your preferences changes back from this Editor to the
* {@link SharedPreferences} object it is editing. This atomically
* performs the requested modifications, replacing whatever is currently
* in the SharedPreferences.
*
* <p>Note that when two editors are modifying preferences at the same
* time, the last one to call commit wins.
*
* <p>If you don't care about the return value and you're
* using this from your application's main thread, consider
* using {@link #apply} instead.
*
* @return Returns true if the new values were successfully written
* to persistent storage.
*/
boolean commit();

/**
* Commit your preferences changes back from this Editor to the
* {@link SharedPreferences} object it is editing. This atomically
* performs the requested modifications, replacing whatever is currently
* in the SharedPreferences.
*
* <p>Note that when two editors are modifying preferences at the same
* time, the last one to call apply wins.
*
* <p>Unlike {@link #commit}, which writes its preferences out
* to persistent storage synchronously, {@link #apply}
* commits its changes to the in-memory
* {@link SharedPreferences} immediately but starts an
* asynchronous commit to disk and you won't be notified of
* any failures. If another editor on this
* {@link SharedPreferences} does a regular {@link #commit}
* while a {@link #apply} is still outstanding, the
* {@link #commit} will block until all async commits are
* completed as well as the commit itself.
*
* <p>As {@link SharedPreferences} instances are singletons within
* a process, it's safe to replace any instance of {@link #commit} with
* {@link #apply} if you were already ignoring the return value.
*
* <p>You don't need to worry about Android component
* lifecycles and their interaction with <code>apply()</code>
* writing to disk. The framework makes sure in-flight disk
* writes from <code>apply()</code> complete before switching
* states.
*
* <p class='note'>The SharedPreferences.Editor interface
* isn't expected to be implemented directly. However, if you
* previously did implement it and are now getting errors
* about missing <code>apply()</code>, you can simply call
* {@link #commit} from <code>apply()</code>.
*/
void apply();

果不其然, 接口注释中有重要提示. 首先对于commit接口, 源码注释给出了三个信息: 1. 返回值表示是否成功写入到存储器; 2. 两个Editor竞争时, 后调用commit的Editor最终生效; 3. 它原子地写入新值, 如果不关心返回值, 而且是在主线程中调用, 应该考虑apply接口. 第1点和第3点预示着commit方法是同步写入, 可能效率会比较低, 在主线程中调用会引起响应变慢, 同时暗示着apply可能是异步写入. 再来看apply 接口, 源码注释给出了三个信息: 1. commit是同步写入外存, 而apply是先写到内存, 再异步写入到外存, 并且不会给出失败通知; 2. 对于同一个SharedPreference, 如果一个apply在等待时, 另一个editor调用了一个commit, 那么这个commit会被阻塞直到所有异步提交以及这个commit本身完成为止; 3. 它原子地写入新值, 在SharedPreference本身是单例实现的情况下(现在默认情况就是), 将所有的commit替换成apply是安全的, Android Framework保证在改变状态前完成已入队的apply操作, 所以你也不用担心组件生命周期会影响apply.
可见二者的最主要区别就是, commit是同步地, apply是异步的. 那么它们写入SharedPreference时候, 是具体怎么写入的呢? 这就要看SharedPreferencesImpl.EditorImpl的实现了.
先看EditorImpl.commit(), 它首先调用commitToMemory提交到内存, 然后调用SharedPreferencesImpl.this.enqueueDiskWrite把提交结果MemoryCommitResult入队, 再调用MemoryCommitResult.writtenToDiskLatch.await()阻塞直到MemoryCommitResult.setDiskWriteResult被调用, 即该结果已经被处理. 到这里我们知道写入工作分两步, 先写入到内存, 再写入到外存. 稍后我们分别来看这两步具体内容, 这里先看EditorImpl.apply()的实现. 它首先调用commitToMemory提交到内存, 然后新建了一个Runnable awaitCommit内容是MemoryCommitResult.writtenToDiskLatch.await(), 所以这个Runnable中会阻塞直到结果被处理. 然后它把这个Runnable加入到了QueuedWork中, 并且再新建了一个Runnable postWriteRunnable, 里面调用awaitCommit.run(), 并从QueueWork中移除awaitCommit. 最后, 它通过SharedPreferencesImpl.this.enqueueDiskWrite把这个postWriteRunnable入队. 可见, 它是一个异步的提交过程. 以上两个方法的具体实现, 也解释了apply和commit冲突时commit将会阻塞直到所有的apply完成的原因: commit会等待直到自己的提交被执行, 而它之前调用的apply在队列中会先于它处理.
现在清楚了提交过程, 来看Editor的数据是如何写入到内存的.
commitToMemory中, 首先实例化了一个MemoryCommitResult mcr, 并让它保存了一个mMap的引用在mapToWriteToDisk成员中. 存在监听器时, 初始化它的keysModifiedlisteners成员, 并在后面把变化了的键放到keysModified中. 然后遍历mModified, 将新的值写入到mMap中, 或者从mMap中移除预定要移除的值, 最后清空mModified并返回mcr. 简单来说, 它把变化的内容复制到了mMap这个原始内容中.
最后, 来看被加入到队列中的MemoryCommitResult是如何被处理的. 先把源码放上方便各位对照:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};

final boolean isFromSyncCommit = (postWriteRunnable == null);

// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}

QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

查看enqueueDiskWrite函数, 发现它先定义了一个Runnable writeToDiskRunnable, 内容是同步地调用writeToFile(mcr), 递减mDiskWritesInFlight计数(该计数在调用commitToMemory时递增), 调用postWriteRunnable.run. 然后判断是否commit, 判断的方法也是非常暴力: postWriteRunnable == null就是同步commit. 此时先判断当前是否只有自己一个任务在执行, 是的话直接在当前线程上调用writeToDiskRunnable.run然后返回, 否则调用QueueWork.singleThreadExecutor().execute(writeToDiskRunnable)来执行. 我另一篇讲Java并发的博客讲过singleThreadExecutor是什么鬼. 显然, 无论是commit还是apply, 最后都是在用一个单线程池在跑, 跑的内容主要就是writeToFile. 在QueueWork中, 添加的任务都在ConcurrentLinkedQueue sPendingWorkFinishers队列中, 这里我们不再讲该队列的处理.
查看writeToFile的实现, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Note: must hold mWritingToDiskLock
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mcr.changesMade) {
// If the file already exists, but no changes were
// made to the underlying map, it's wasteful to
// re-write the file. Return as if we wrote it
// out.
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false);
return;
}
} else {
mFile.delete();
}
}

// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
FileStatus stat = new FileStatus();
if (FileUtils.getFileStatus(mFile.getPath(), stat)) {
synchronized (this) {
mStatTimestamp = stat.mtime;
mStatSize = stat.size;
}
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
mcr.setDiskWriteResult(true);
return;
} // catch exceptions
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false);
}

源码放在博客里有一丢丢长, 但是除了log以外我都没舍得删, 因为我觉得写得很美, 尤其是虽然代码不多但注释不少. 该函数首先判断是否有改变, 没有改变就返回了, 然后判断备份文件是否存在, 不存在则把当前文件作为备份文件. 备份好了后在原文件位置创建新的文件来操作, 把整个mcr.mapToWriteToDisk写入到新文件中, 一切顺利的情况下, 更新SharedPreferencesImpl文件状态信息, 删除备份文件, 并设置写入结果为true, 然后返回. 一旦出错, 就会执行到后面的扫尾代码, 此时将新文件删除, 并设置写入结果为false. 注释里面有一个小提示, Attempt to write the file, delete the backup and return true as atomically as possible, 也就是说这个写入过程可能不是原子的, 只是实现上尽量保证原子而已.
至此, SharedPreference的实现原理我们已经大致了解.

SharedPreference优缺点

SharedPreference的优点是显而易见的: 系统实现, 拿来即可用; 接口定义清晰, 容易使用; 采用Map数据结构实现, 可以存储对象; 在Application中全局可见, 并且全局单例.
同时它的缺点也是很明显:

  1. 从第一节的分析可以看出, 每次有变化时, 系统默认是将整个SharedPreference文件备份, 写入新文件, 再删除备份文件. 这就意味着SharedPreference不适合存储大量内容, 也不适合对内容改动很少但很频繁的场景, 否则会因为文件读写而存在性能瓶颈. 前者是天然限制, 而后者可以通过尽量少调用commit和apply来规避;
  2. apply中混用commit可能导致效率降低. 原因在上面分析中讲过, commit会等待前面所有的apply完成, 也就意味着无论前面用了多少次apply, 一旦后面跟着一个commit, 对于commit所在的线程来说, apply的优势就荡然无存了.
    另外, 由于该文件是通过应用创建的, 所以可以直接在应用数据目录下看到文件内容, 安全性不能保证, 不过这就不在本文讨论范围之内了.

结语

分析了这些, 对于我们有什么用呢? 虽然我很讨厌什么都用”有没有用”来衡量, 但不写个结论确实也感觉文章虎头蛇尾. SharedPreference源码分析至少给我们几点启示:

  1. 注意SharedPreference的使用场景, 尽量规避它所不擅长的情景. 使用时如果对实时性和写入结果要求不高, 尽量只使用apply提交改动.
  2. 完全可以仿照源码自行实现SharedPreference读写接口. 一个应用显然可以有多个SharedPreference文件, 将强耦合的内容分别聚拢到各自的SharedPreference文件中, 理论上可以提高读写效率, 如果有更高效的SharedPreference内容管理方式, 可以完全抛开系统实现, 自行管理SharedPreference文件.
  3. SharedPreference的访问权限是创建时决定的, 依赖于文件创建模式参数, 不一定只有本应用可以访问.
  4. 它一点也不神秘, 甚至在Android系统架构中地位也不太重要, 任何时候它被别的方式取代我也不会吃惊, 但它的实现代码, 透露着一种简洁的优雅.

本篇博客没有参考文献, 唯一参考来源是Android 4.0源码及其注释, 当然自己写来验证想法的Demo不算.

文章目录
  1. 1. SharedPreference实现
  2. 2. SharedPreference优缺点
  3. 3. 结语